"""Composure Glulx virtual machine

Usage: "python glulx.py <game>"
"""

from struct import *
import gtk, warnings
import execute, glk, memory, opcodes, vm

class Glulx(vm.VirtualMachine):
	"The Composure Glulx virtual machine"
	name = "Glulx"

	def __init__(self, game, window = None):
		vm.VirtualMachine.__init__(self, window)

		#	Load the game headers
		headerNames = ('MAGIC', 'VERSION', 'RAMSTART', 'EXTSTART', 'ENDMEM', 'STACKSIZE', 'MAINFUNC', 'DECODING', 'CHECKSUM')
		headerData = unpack('>9L', game.read(36))
		self.headers = dict([(headerNames[i], headerData[i]) for i in range(9)])

		#	Check the game file validity
		if self.headers['MAGIC'] != 0x476C756C:
			raise GlulxError("Not a Glulx game file")
		if not 0x00020000 <= self.headers['VERSION'] <= 0x000301FF:
			raise GlulxError("Glulx version not supported")
		if not self.headers['RAMSTART'] <= self.headers['EXTSTART'] <= self.headers['ENDMEM']:
			raise GlulxError("Memory segment boundaries are ordered badly")
		if self.headers['RAMSTART'] & 0xFF or self.headers['EXTSTART'] & 0xFF or self.headers['ENDMEM'] & 0xFF:
			warnings.warn("Memory segment boundaries are not 256-byte aligned", GlulxWarning)

		#	Add a few IO systems
		self.IOSystems.extend([GlulxFilter(), glk.Glk(self.window)])

		#	Game file is valid, so start it up!
		self.game = game
		self.memory = memory.GlulxMemoryMap(self)
		self.registers = {}
		self.restart()
		self.executeLoop()

	def restart(self):
		"Restart Glulx"
		#	Set up memory, stack and registers
		self.memory.reset()
		self.stack = memory.GlulxStack(self)
		self.registers.update({
			'endmem': self.headers['ENDMEM'],
			'frameptr': 0,
			'localsbase': 0,
			'stringtable': 0,
			'valstackbase': 0})

		#	Start the game
		self.enterFunction(self.headers['MAINFUNC'], 0, None)

	def enterFunction(self, addr, argc, argv):
		"Enter a Glulx function"
		#	Check function type
		funcType = self.memory.read('B', addr)[0]
		if funcType != 0xC0 and funcType != 0xC1:
			if 0xC0 <= funcType <= 0xDF:
				raise GlulxError("Call to a function of unknown type")
			else:
				raise GlulxError("Call to non-function")

		#	Bump the frame pointer
		self.registers['frameptr'] = self.stack.curPos
		
		#	Copy the locals format list to the call frame
		index = 0
		localslen = 0
		self.stack.curPos = self.registers['frameptr'] + 8
		while 1:
			loctype = self.memory.read('B')[0]
			locnum = self.memory.read('B')[0]
			self.stack.write('2B', (loctype, locnum))
			index += 1

			#	Check if we've copied all the locals, and if so ensure the call frame is 4-byte aligned
			if loctype == 0:
				if index & 1:
					self.stack.write('2B', (0, 0))
					index += 1
				break

			#	Increase the locals length, padding if needed
			if loctype == 4:
				while localslen & 3:
					localslen += 1
			elif loctype == 2:
				while localslen & 1:
					localslen += 1
			elif loctype == 1:
				pass
			else:
				raise GlulxError("Illegal local type in locals format list")
			localslen += loctype * locnum

		#	Pad the locals once more
		while localslen & 3:
			localslen += 1

		#	Set some registers
		self.registers['localbase'] = self.stack.curPos
		self.registers['valstackbase'] = self.registers['localbase'] + localslen

		#	Check for stack overflow
		if self.registers['valstackbase'] >= self.headers['STACKSIZE']:
			raise GlulxError("Stack overflow in function call")

		#	Fill in the call frame
		self.stack.write('2L', (8 + 2 * index + localslen, 8 + 2 * index), self.registers['frameptr'])

		#	Zero out the locals
		self.stack.write('B' * localslen, [0] * localslen, self.registers['localbase'])

		if funcType == 0xC0:
			#	Push the function arguments onto the stack
			if self.registers['valstackbase'] + localslen + 4 * (argc + 1) >= self.headers['STACKSIZE']:
				raise GlulxError("Stack overflow in function arguments")
			#	Cast the arguments just to make sure that we can reverse them...
			argv = list(argv)
			argv.reverse()
			argv.append(argc)
			self.stack.write('L' * len(argv), argv)
		else:
			#	Copy in function arguments
			ix = 0
			modeaddr = self.registers['frameptr'] + 8
			self.stack.curPos = self.registers['localbase']
			while ix < argc:
				loctype = self.stack[modeaddr]
				modeaddr += 1
				locnum = self.stack[modeaddr]
				modeaddr += 1
				if loctype == 0:
					break
				if loctype == 4:
					while self.stack.curPos & 3:
						self.stack.curPos += 1
					while ix < argc and locnum:
						self.stack.write('L', (argv[ix], ))
						ix += 1
						locnum -= 1
				elif loctype == 2:
					while self.stack.curPos & 1:
						self.stack.curPos += 1
					while ix < argc and locnum:
						self.stack.write('H', (argv[ix] & 0xFFFF, ))
						ix += 1
						locnum -= 1
				elif loctype == 1:
					while ix < argc and locnum:
						self.stack.write('B', (argv[ix] & 0xFF, ))
						ix += 1
						locnum -= 1

			self.stack.curPos = self.registers['valstackbase']

	def popArguments(self, count, addr):
		"Pop arguments off the stack"
		if count == 0:
			return None

		if not addr:
			#	Pop arguments off the stack
			print "stackptr: %s" % self.stack.curPos
			print "valstackbase: %s" % (self.registers['valstackbase'] + 4 * count)
			if self.stack.curPos < self.registers['valstackbase'] + 4 * count:
				raise GlulxError("Stack underflow in arguments")
			self.stack.curPos -= 4 * count
			arguments = list(self.stack.read('L' * count, update = False))
			arguments.reverse()
		else:
			#	Get arguments from main memory
			arguments = self.memory.read('L' * count, addr, False)

		return arguments

	#	Add some more functions found in other files
	executeLoop = execute.executeLoop
	lookupOperands = opcodes.lookupOperands
	parseOperands = opcodes.parseOperands
	storeOperand = opcodes.storeOperand

class GlulxFilter(vm.IOSystem):
	"A filter IO system"
	pass

class GlulxError(Exception):
	pass

class GlulxWarning(Warning):
	pass

if __name__ == '__main__':
	#	Check which game is being loaded
	import sys
	try:
		filename = sys.argv[1]
	except IndexError:
		sys.exit(__doc__)

	#	Open the game's file
	try:
		game = open(filename, 'rb')
	except IOError:
		sys.exit("Error opening game file")

	#	Create an instance of Glulx and run the game
	try:
		glulx = Glulx(game)
		glulx.window.show()
		gtk.main()
	except GlulxError, e:
		sys.exit("Glulx error: %s" % e)
